/*
日志落地模块，负责将日志数据落地到指定位置，如文件、数据库、消息队列等。
*/

#ifndef _SINK_HPP_
#define _SINK_HPP_

#include "util.hpp"
#include <memory>
#include <fstream>
#include <cassert>
#include <sys/types.h>
#include <dirent.h>
#include <cstring>
#include <sstream>

namespace xzt
{
    namespace log
    {
        class LogSink  // 日志落地基类
        {
        public:
            using ptr = std::shared_ptr<LogSink>;

            virtual void log(const char* msg, size_t len) = 0;
            virtual ~LogSink() = default;
        };

        // 落地方向：标准输出
        class StdoutSink : public LogSink
        {
        public:
            // 参数说明：无参构造
            StdoutSink() = default;

            void log(const char* msg, size_t len) override
            {
                std::cout.write(msg, len);
                assert(std::cout.good());
                std::cout.flush();
            }
        };

        // 落地方向：指定文件
        class FileSink : public LogSink
        {
        public:
            // 参数说明：
            // pathname: 日志文件的路径名，如"/log/data.log"
            FileSink(const std::string& pathname) : _pathname(pathname)
            {
                // 创建目录
                util::File::createDirectory(util::File::path(_pathname));
                // 打开文件
                _ofs.open(_pathname, std::ios::app);
                assert(_ofs.is_open());
            }

            ~FileSink()
            {
                _ofs.close();
            }

            void log(const char* msg, size_t len) override
            {
                _ofs.write(msg, len);
                assert(_ofs.good());
                _ofs.flush();
            }
        private:
            std::string _pathname;
            std::ofstream _ofs;
        };

        // 滚动文件辅助工具类
        class RollingFileHelper
        {
        public:
            // 找到指定目录下最后一个日志文件名，如果不存在则返回nullptr
            static const char* findLastFile_constRet(const std::string& base_pathname)
            {
                return findLastFile(base_pathname);
            }
            static char* findLastFile(const std::string& base_pathname)
            {
                size_t pos = base_pathname.rfind('/');
                std::string directory = "./";
                if(pos != std::string::npos)
                {
                    directory = base_pathname.substr(0, pos + 1);
                }
                
                DIR* dir = opendir(directory.c_str());
                if(dir == nullptr)
                {
                    std::cerr << "RollingFileHelper::findLastFile() error" << std::endl;
                    std::cerr << "opendir(" << directory << ") failed" << std::endl;
                    perror("opendir");
                    return nullptr;
                }

                // 获取日志文件的前缀名
                const char* head_filename = base_pathname.substr(pos + 1).c_str();
                // 设置静态变量last_filename
                static char last_filename[128];
                strncpy(last_filename, head_filename, sizeof(last_filename) - 1);

                // 找到最后一个日志文件名
                struct dirent* entry;
                while((entry = readdir(dir)) != nullptr)
                {
                    // 找到DT_REG常规文件而非DT_DIR目录，且文件名以head_filename开头
                    if(entry->d_type == DT_REG && strncmp(entry->d_name, head_filename, strlen(head_filename)) == 0)
                    {
                        if(strcmp(entry->d_name, last_filename) > 0)  // 找到更大的日志文件名
                        {
                            memset(last_filename, 0, sizeof(last_filename));  // 清空last_filename
                            strncpy(last_filename, entry->d_name, sizeof(last_filename) - 1);
                            last_filename[sizeof(last_filename) - 1] = '\0';  // 确保字符串以'\0'结尾
                        }
                    }
                }
                closedir(dir);
                // 判断last_filename是否有效，无效对应于没有找到日志文件的情况
                bool is_valid = (strcmp(last_filename, head_filename) != 0);
                return is_valid? last_filename : nullptr;
            }
        };


        // 落地方向：滚动文件（按大小分割）
        class RollingBySizeFileSink : public LogSink
        {
        public:
            // 参数说明：
            // base_pathname: 日志文件的基本路径名，如"/log/data-"，由于是滚动文件，会在此基础上自动添加日期、时间等信息
            // max_size: 单个日志文件的最大大小，单位字节
            RollingBySizeFileSink(const std::string& base_pathname, size_t max_size) :
                _base_pathname(base_pathname), _max_size(max_size), _cur_size(0), _count(0)
            {
                // (若不存在)创建目录
                std::string directory = util::File::path(_base_pathname);
                util::File::createDirectory(directory);

                // 获取最后一个日志文件名，如果不存在则创建一个
                const char* last_filename = RollingFileHelper::findLastFile_constRet(_base_pathname);
                if (last_filename == nullptr)
                {
                    // 获取当前时间，并创建日志文件
                    std::string filename = _createFileName();
                    _ofs.open(filename, std::ios::binary | std::ios::app);  // 以二进制方式打开文件，以追加方式写入
                    assert(_ofs.is_open());
                    last_filename = filename.c_str();
                }
                else  // 此时last_filename指向最后一个日志文件名
                {
                    // 打开最后一个日志文件
                    _ofs.open(directory + last_filename, std::ios::binary | std::ios::app);  // 以二进制方式打开文件，以追加方式写入
                    assert(_ofs.is_open());
                    // 记得计算当前日志文件大小，否则后面log()会出错
                    _cur_size = util::File::size(directory + last_filename);  
                }
            }

            ~RollingBySizeFileSink()
            {
                _ofs.close();
            }

            void log(const char* msg, size_t len) override
            {
                // 此时_ofs已打开，且指向当前日志文件
                if(_cur_size + len > _max_size)  // 当前日志文件已满
                {
                    // 关闭当前日志文件，并创建新的日志文件
                    _ofs.close();
                    std::string filename = _createFileName();
                    _ofs.open(filename, std::ios::binary | std::ios::app);  // 以二进制方式打开文件，以追加方式写入
                    assert(_ofs.is_open());
                    _cur_size = 0;
                }

                // 写入日志数据
                _ofs.write(msg, len);
                assert(_ofs.good());
                _ofs.flush();
                _cur_size += len;
            }
        private:
            std::string _createFileName()
            {
                time_t current_time = util::Date::now();
                struct tm tm_time;
                localtime_r(&current_time, &tm_time);
                
                std::stringstream ss;
                ss << _base_pathname;
                ss << tm_time.tm_year + 1900 << "-" << tm_time.tm_mon + 1 << "-" << tm_time.tm_mday;
                ss << "_" << tm_time.tm_hour << "-" << tm_time.tm_min << "-" << tm_time.tm_sec;
                ss << "_" << _count++ << ".log";
                return ss.str();
            }
        private:
            std::string _base_pathname;
            std::ofstream _ofs;
            size_t _max_size;
            size_t _cur_size;
            size_t _count;  // 日志文件计数器:防止一秒内超过max_size本该分页的日志写到同一个文件
        };

        // 落地方向类对象工厂
        class SinkFactory
        {
        public:
            template<typename SinkType, typename... Args>
            static typename SinkType::ptr create(Args&&... args)
            {
                return std::make_shared<SinkType>(std::forward<Args>(args)...);
            }
        };
    }
}





#endif // _SINK_HPP_